//@version=6
indicator("Strong SFP MSS", shorttitle="Strong SFP MSS", overlay=true,
     max_lines_count=500, max_labels_count=500, max_boxes_count=500, max_bars_back=500)

string G_LVL = "━━ Liquidity Levels ━━"
string G_SES = "━━ Sessions (level origin) ━━"
string G_KEY = "━━ Key Liquidity (PDH · PDL · PWH · PWL) ━━"
string G_EVT = "━━ Sweep Classification ━━"
string G_SIG = "━━ Signals · Honest Outcomes ━━"
string G_VIS = "━━ Neon Visuals ━━"
string G_DSH = "━━ Dashboard ━━"

int    prdInput      = input.int(10, "Pivot Period", minval=2, maxval=100, group=G_LVL,
     tooltip="Swing strength. A pivot high needs this many lower highs on both sides. Levels are confirmed this many bars after the pivot and only if price has not already broken them.")
int    maxActInput   = input.int(5, "Max Active Levels Per Side", minval=1, maxval=10, group=G_LVL,
     tooltip="Oldest unswept level is retired when the cap is hit.")
int    maxAgeInput   = input.int(0, "Max Level Age (bars · 0 = unlimited)", minval=0, maxval=5000, group=G_LVL)
bool   eqMergeInput  = input.bool(true, "Merge Equal Highs / Lows Into Pools", group=G_LVL,
     tooltip="A new pivot within the tolerance of an active level on the same side is merged into it (touches +1, strength up) instead of drawing a second line.")
float  eqTolInput    = input.float(0.25, "Equal-Level Tolerance (×ATR)", minval=0.05, maxval=2.0, step=0.05, group=G_LVL)
int    atrLenInput   = input.int(14, "ATR Length (scaling unit)", minval=2, maxval=200, group=G_LVL)

string tzInput       = input.string("America/New_York", "Timezone", options=["America/New_York", "UTC-4", "UTC-5", "UTC", "Europe/London", "Asia/Bangkok", "Asia/Tokyo"], group=G_SES)
string asiaSess      = input.session("2000-0000", "Asia", group=G_SES, inline="s1")
color  asiaColInput  = input.color(color.rgb(255, 230, 0), "", group=G_SES, inline="s1")
string lonSess       = input.session("0200-0500", "London", group=G_SES, inline="s2")
color  lonColInput   = input.color(color.rgb(0, 220, 255), "", group=G_SES, inline="s2")
string nyAmSess      = input.session("0830-1100", "NY AM", group=G_SES, inline="s3")
color  nyAmColInput  = input.color(color.rgb(255, 140, 0), "", group=G_SES, inline="s3")
string nyPmSess      = input.session("1330-1600", "NY PM", group=G_SES, inline="s4")
color  nyPmColInput  = input.color(color.rgb(255, 0, 200), "", group=G_SES, inline="s4")
color  otherColInput = input.color(color.rgb(150, 160, 190), "Outside Sessions", group=G_SES)

bool   showKeyInput  = input.bool(true, "Show Key Liquidity Lines", group=G_KEY,
     tooltip="Prior-day and prior-week high/low from the last CLOSED day/week (non-repainting). Drawn as neon targets and listed in the Liquidity Map.")
bool   keyDayInput   = input.bool(true, "Prior Day High / Low", group=G_KEY, inline="k1")
bool   keyWeekInput  = input.bool(true, "Prior Week High / Low", group=G_KEY, inline="k1")
int    keyLenInput   = input.int(60, "Key Line Length (bars)", minval=10, maxval=500, group=G_KEY)

bool   showXInput    = input.bool(true, "Show SWEEP · Wick Raid",    group=G_EVT, inline="e1")
bool   showSfpInput  = input.bool(true, "SFP · Reclaim",          group=G_EVT, inline="e1")
bool   showMssInput  = input.bool(true, "MSS · Shift",            group=G_EVT, inline="e1")
float  mssDispInput  = input.float(0.0, "MSS Displacement Filter (×ATR · 0 = off)", minval=0.0, maxval=3.0, step=0.1, group=G_EVT,
     tooltip="When > 0, an MSS needs the close to be at least this far beyond the level. Until then the level stays in the PIERCED state (waiting for reclaim or acceptance).")
int    patienceInput = input.int(6, "Pierce Patience (bars)", minval=1, maxval=100, group=G_EVT,
     tooltip="A level that stays closed-through for this many bars without a reclaim is booked as MSS by acceptance, even without a lower-low / higher-high close.")
float  proxInput     = input.float(0.5, "Proximity Alert (×ATR)", minval=0.1, maxval=5.0, step=0.1, group=G_EVT)

float  minStrInput   = input.float(3.0, "Min Signal Strength (0-10)", minval=0.0, maxval=10.0, step=0.5, group=G_SIG,
     tooltip="Every classification is drawn. Only classifications at or above this strength become tracked signals (target / invalidation lines, outcome labels, statistics, alerts).")
string stopModeInput = input.string("Structural", "Stop Model", options=["Structural", "ATR"], group=G_SIG,
     tooltip="Structural: X / SFP risk = distance to the sweep extreme + buffer; MSS risk = distance back to the level + buffer. ATR: risk = Invalidation × ATR for every setup. Risk is floored at Min Risk.")
float  bufInput      = input.float(0.15, "Structural Buffer (×ATR)", minval=0.0, maxval=2.0, step=0.05, group=G_SIG, inline="r1")
float  minRiskInput  = input.float(0.5, "Min Risk (×ATR)", minval=0.1, maxval=5.0, step=0.1, group=G_SIG, inline="r1")
float  tgtRInput     = input.float(1.5, "Target (R)", minval=0.25, maxval=10.0, step=0.25, group=G_SIG, inline="r2",
     tooltip="Target = this many R from the entry close. Invalidation (ATR stop model only) = this many ATR against. A coin flip at 1.5R / 1R scores 40% — WIN% above that is edge.")
float  invUInput     = input.float(1.0, "Invalidation (×ATR · ATR model)", minval=0.25, maxval=5.0, step=0.25, group=G_SIG, inline="r2")
int    maxHoldInput  = input.int(60, "Outcome Max Hold (bars)", minval=5, maxval=1000, group=G_SIG,
     tooltip="A signal that hits neither level within this many bars is booked as TIMEOUT (marked to market, counted as a non-win).")
bool   showOutInput  = input.bool(true, "Mark Signal Outcomes", group=G_SIG, inline="o1")
bool   showTgtInput  = input.bool(true, "Target / Invalidation Lines", group=G_SIG, inline="o1")

string paletteInput  = input.string("Neon Violet / Green", "Palette",
     options=["Neon Violet / Green", "Neon Magenta / Cyan", "Neon Amber / Ice", "Custom"], group=G_VIS)
color  bearCustom    = input.color(color.rgb(170, 60, 255), "Custom Bearish", group=G_VIS, inline="cc")
color  bullCustom    = input.color(color.rgb(0, 255, 140),  "Bullish",        group=G_VIS, inline="cc")
color  colXInput     = input.color(color.rgb(236, 236, 255), "SWEEP",   group=G_VIS, inline="ek")
color  colSfpInput   = input.color(color.rgb(255, 47, 160),  "SFP", group=G_VIS, inline="ek")
color  colMssInput   = input.color(color.rgb(0, 229, 255),   "MSS", group=G_VIS, inline="ek")
string styXInput     = input.string("Dotted", "SWEEP Line",   options=["Solid", "Dashed", "Dotted"], group=G_VIS, inline="es")
string stySfpInput   = input.string("Dashed", "SFP Line", options=["Solid", "Dashed", "Dotted"], group=G_VIS, inline="es")
string styMssInput   = input.string("Solid",  "MSS Line", options=["Solid", "Dashed", "Dotted"], group=G_VIS, inline="es")
int    widthInput    = input.int(2, "Active Line Width", minval=1, maxval=4, group=G_VIS, inline="v0")
bool   glowInput     = input.bool(true, "Neon Glow", group=G_VIS, inline="v0")
int    extInput      = input.int(15, "Line Extension (bars)", minval=0, maxval=100, group=G_VIS)
string kindSizeInput = input.string("Large", "Event Text Size", options=["Tiny", "Small", "Normal", "Large", "Huge"], group=G_VIS, inline="t1")
float  offAtrInput   = input.float(0.6, "Text Distance (×ATR)", minval=0.0, maxval=5.0, step=0.1, group=G_VIS, inline="t1")
bool   flipInput     = input.bool(false, "Flip Text Position", group=G_VIS, inline="t2")
bool   showPriceInput = input.bool(true, "Price Tags (active levels)", group=G_VIS, inline="t2")
bool   showSigLblInput = input.bool(true, "Signal Tags (★ strength)", group=G_VIS, inline="t3")
bool   showSweepBox  = input.bool(true, "Sweep Boxes", group=G_VIS, inline="t3")
string tagSizeInput  = input.string("Small", "Tag Size", options=["Tiny", "Small", "Normal"], group=G_VIS)
int    maxHistInput  = input.int(80, "Event History Kept", minval=10, maxval=200, group=G_VIS,
     tooltip="Visual cap only. Statistics are cumulative over every bar and are never touched by recycling.")

bool   dashOn    = input.bool(true, "Show Dashboard", group=G_DSH)
string dashPos   = input.string("Top Right", "Position", options=["Top Right","Middle Right","Bottom Right","Top Left","Middle Left","Bottom Left"], group=G_DSH)
string dashSizeS = input.string("Small", "Text Size", options=["Tiny","Small","Normal","Large"], group=G_DSH)
bool   showP1    = input.bool(true, "Panel I · Liquidity State",     group=G_DSH)
bool   showP2    = input.bool(true, "Panel II · Liquidity Map",      group=G_DSH)
bool   showP3    = input.bool(true, "Panel III · Setup Analytics",   group=G_DSH)
bool   showP4    = input.bool(true, "Panel IV · Session Analytics",  group=G_DSH)
bool   showP5    = input.bool(true, "Panel V · Sweep Statistics",    group=G_DSH)
int    panelTr   = input.int(6, "Panel Transparency", minval=0, maxval=90, step=2, group=G_DSH)
float  sepH      = input.float(0.9, "Separator Gap (% height)", minval=0.1, maxval=4.0, step=0.1, group=G_DSH)
bool   monoFont  = input.bool(true, "Monospace Numbers", group=G_DSH)

color BEAR_T1 = switch paletteInput
    "Neon Violet / Green" => color.rgb(214, 160, 255)
    "Neon Magenta / Cyan" => color.rgb(255, 150, 220)
    "Neon Amber / Ice"    => color.rgb(255, 214, 140)
    =>                       color.new(bearCustom, 0)
color BEAR_T3 = switch paletteInput
    "Neon Violet / Green" => color.rgb(128, 32, 255)
    "Neon Magenta / Cyan" => color.rgb(255, 0, 150)
    "Neon Amber / Ice"    => color.rgb(255, 120, 0)
    =>                       color.new(bearCustom, 0)
color BULL_T1 = switch paletteInput
    "Neon Violet / Green" => color.rgb(170, 255, 196)
    "Neon Magenta / Cyan" => color.rgb(150, 245, 255)
    "Neon Amber / Ice"    => color.rgb(170, 225, 255)
    =>                       color.new(bullCustom, 0)
color BULL_T3 = switch paletteInput
    "Neon Violet / Green" => color.rgb(0, 230, 96)
    "Neon Magenta / Cyan" => color.rgb(0, 200, 255)
    "Neon Amber / Ice"    => color.rgb(0, 140, 255)
    =>                       color.new(bullCustom, 0)

f_tint(color _c, float _toWhite) =>
    float r = color.r(_c)
    float g = color.g(_c)
    float b = color.b(_c)
    color.rgb(r + (255.0 - r) * _toWhite, g + (255.0 - g) * _toWhite, b + (255.0 - b) * _toWhite)

color bearSoft   = paletteInput == "Custom" ? f_tint(bearCustom, 0.45) : BEAR_T1
color bullSoft   = paletteInput == "Custom" ? f_tint(bullCustom, 0.45) : BULL_T1
color bearAccent = paletteInput == "Custom" ? bearCustom : BEAR_T3
color bullAccent = paletteInput == "Custom" ? bullCustom : BULL_T3

const int GLOW_T  = 82
const int BOX_T   = 86
const int BOX_BRD = 55

color TXT     = #d9dce6
color MUTED   = #7d8597
color DIM     = #4a5163
color GOLD    = #f5b942
color WHITE   = #e8e6f0
color CLEAR   = color.new(#000000, 100)
color PANEL   = color.new(#0b0e15, panelTr)
color PANEL2  = color.new(#10141e, panelTr)
color HEAD    = color.new(#181e2c, panelTr)
color COLHD   = color.new(#131826, panelTr)
color PRICEBG = color.new(#1d2333, panelTr)

f_clamp(float _v, float _lo, float _hi) => math.max(_lo, math.min(_hi, _v))
f_px(float v)  => na(v) ? "—" : str.tostring(v, format.mintick)
f_sn(float v, string fmt) => (v > 0 ? "+" : "") + str.tostring(v, fmt)
f_pct(int n, int d) => d > 0 ? str.tostring(float(n) / d * 100.0, "#") + "%" : "—"
f_frac(int n, int d) => d > 0 ? float(n) / d : 0.0
f_stars(float sc) => sc >= 8 ? "★★★★" : sc >= 6.5 ? "★★★" : sc >= 5 ? "★★" : sc >= 3 ? "★" : "·"
f_tierWord(float _s) => _s >= 7.5 ? "EXTREME" : _s >= 5.0 ? "STRONG" : "MILD"
f_bucket(float _s) => _s >= 7.5 ? 2 : _s >= 5.0 ? 1 : 0

f_gauge(float frac, int n) =>
    int k = math.max(0, math.min(n, int(math.round(frac * n))))
    str.repeat("▰", k) + str.repeat("▱", n - k)

f_style(string s) => s == "Dotted" ? line.style_dotted : s == "Dashed" ? line.style_dashed : line.style_solid

f_textOn(color _bg) =>
    float lum = 0.299 * color.r(_bg) + 0.587 * color.g(_bg) + 0.114 * color.b(_bg)
    lum >= 150.0 ? color.rgb(8, 8, 12) : color.rgb(226, 228, 238)

f_size5(string s) => s == "Tiny" ? size.tiny : s == "Small" ? size.small : s == "Normal" ? size.normal : s == "Huge" ? size.huge : size.large
f_size3(string s) => s == "Tiny" ? size.tiny : s == "Normal" ? size.normal : size.small

f_inSess(string _s) => not na(time(timeframe.period, _s + ":1234567", tzInput))

f_sessName(int s) => s == 1 ? "ASIA" : s == 2 ? "LONDON" : s == 3 ? "NY AM" : s == 4 ? "NY PM" : "OFF"
f_sessShort(int s) => s == 1 ? "AS" : s == 2 ? "LN" : s == 3 ? "NA" : s == 4 ? "NP" : "··"
f_sessCol(int s) => s == 1 ? asiaColInput : s == 2 ? lonColInput : s == 3 ? nyAmColInput : s == 4 ? nyPmColInput : otherColInput
f_sessW(int s) => s == 3 ? 1.5 : s == 2 ? 1.0 : s == 4 ? 1.0 : s == 1 ? 0.5 : 0.0

f_kindName(int k) => k == 0 ? "SWEEP" : k == 1 ? "SFP" : "MSS"
f_kindCol(int k) => k == 0 ? colXInput : k == 1 ? colSfpInput : colMssInput
f_kindStyle(int k) => k == 0 ? f_style(styXInput) : k == 1 ? f_style(stySfpInput) : f_style(styMssInput)
f_kindShow(int k) => k == 0 ? showXInput : k == 1 ? showSfpInput : showMssInput

f_nextTf(string _tf) =>
    _tf == "1" ? "5" : _tf == "5" ? "15" : _tf == "15" ? "30" : _tf == "30" ? "60" : _tf == "60" ? "240" : _tf == "240" ? "1D" : _tf == "1D" ? "1W" : _tf == "1W" ? "1M" : ""

f_prettyTf(string _tf) =>
    _tf == "1" ? "1m" : _tf == "5" ? "5m" : _tf == "15" ? "15m" : _tf == "30" ? "30m" : _tf == "60" ? "1H" : _tf == "120" ? "2H" : _tf == "240" ? "4H" : _tf == "1D" ? "1D" : _tf == "1W" ? "1W" : _tf == "1M" ? "1M" : _tf

float atrUnit   = ta.atr(atrLenInput)
float atrSafe   = na(atrUnit) or atrUnit <= 0.0 ? syminfo.mintick * 10.0 : atrUnit
float volAvg20  = ta.sma(volume, 20)
bool  hasVol    = not na(volume) and volAvg20 > 0.0
float volRatio  = hasVol ? nz(volume) / volAvg20 : 1.0
float rangeBar  = math.max(high - low, syminfo.mintick)
float wickUpR   = (high - math.max(open, close)) / rangeBar
float wickDnR   = (math.min(open, close) - low) / rangeBar
int   sessNow   = f_inSess(nyPmSess) ? 4 : f_inSess(nyAmSess) ? 3 : f_inSess(lonSess) ? 2 : f_inSess(asiaSess) ? 1 : 0

float ph = ta.pivothigh(high, prdInput, prdInput)
float pl = ta.pivotlow(low, prdInput, prdInput)

int   pivBar    = bar_index - prdInput
int   pivSess   = sessNow[prdInput]
float pivVol    = volRatio[prdInput]
float pivWickUp = wickUpR[prdInput]
float pivWickDn = wickDnR[prdInput]

float biasRaw = (close - ta.ema(close, 50)) / atrSafe
string tf1 = f_nextTf(timeframe.period)
string tf2 = f_nextTf(tf1)
string tf3 = f_nextTf(tf2)
string tf1Safe = tf1 == "" ? timeframe.period : tf1
string tf2Safe = tf2 == "" ? timeframe.period : tf2
string tf3Safe = tf3 == "" ? timeframe.period : tf3
float b1raw = request.security(syminfo.tickerid, tf1Safe, biasRaw[1], lookahead=barmerge.lookahead_on)
float b2raw = request.security(syminfo.tickerid, tf2Safe, biasRaw[1], lookahead=barmerge.lookahead_on)
float b3raw = request.security(syminfo.tickerid, tf3Safe, biasRaw[1], lookahead=barmerge.lookahead_on)
float b1 = tf1 == "" ? na : b1raw
float b2 = tf2 == "" ? na : b2raw
float b3 = tf3 == "" ? na : b3raw

bool keyDayOk  = timeframe.in_seconds(timeframe.period) < timeframe.in_seconds("D")
bool keyWeekOk = timeframe.in_seconds(timeframe.period) < timeframe.in_seconds("W")
[pdHraw, pdLraw] = request.security(syminfo.tickerid, "D", [high[1], low[1]], lookahead=barmerge.lookahead_on)
[pwHraw, pwLraw] = request.security(syminfo.tickerid, "W", [high[1], low[1]], lookahead=barmerge.lookahead_on)
float pdH = keyDayInput and keyDayOk ? pdHraw : na
float pdL = keyDayInput and keyDayOk ? pdLraw : na
float pwH = keyWeekInput and keyWeekOk ? pwHraw : na
float pwL = keyWeekInput and keyWeekOk ? pwLraw : na

var float dayHi = na
var float dayLo = na
var float wkHi  = na
var float wkLo  = na
if timeframe.change("D") or na(dayHi)
    dayHi := high
    dayLo := low
else
    dayHi := math.max(dayHi, high)
    dayLo := math.min(dayLo, low)
if timeframe.change("W") or na(wkHi)
    wkHi := high
    wkLo := low
else
    wkHi := math.max(wkHi, high)
    wkLo := math.min(wkLo, low)
bool pdhSwept = not na(pdH) and dayHi >= pdH
bool pdlSwept = not na(pdL) and dayLo <= pdL
bool pwhSwept = not na(pwH) and wkHi >= pwH
bool pwlSwept = not na(pwL) and wkLo <= pwL

type Lvl
    int   dir
    float price
    int   pbar
    int   sess
    int   touches
    float score
    bool  pierced
    int   pierceBar
    float extreme
    line  ln
    line  glow
    label tag

type Sig
    int   dir
    int   kind
    int   sess
    int   bucket
    float entry
    float target
    float invalid
    float risk
    int   ebar
    float mfe
    float mae
    line  lnT
    line  lnX

var array<Lvl>   lvls   = array.new<Lvl>()
var array<Sig>   sigs   = array.new<Sig>()
var array<line>  hLines = array.new<line>()
var array<label> hLbls  = array.new<label>()
var array<box>   hBoxes = array.new<box>()
var array<label> outLbls = array.new<label>()

var line  pdhLn = na
var line  pdlLn = na
var line  pwhLn = na
var line  pwlLn = na
var label pdhLb = na
var label pdlLb = na
var label pwhLb = na
var label pwlLb = na

var array<int>   evKind  = array.new<int>(3, 0)
var array<float> evDepth = array.new<float>(3, 0.0)
var array<float> evAge   = array.new<float>(3, 0.0)
var array<int>   sesLvl  = array.new<int>(5, 0)
var array<int>   sesEv   = array.new<int>(15, 0)
var array<int>   sesSigN = array.new<int>(5, 0)
var array<int>   sesSigW = array.new<int>(5, 0)
var int   nLvlCreated = 0
var int   nLvlExpired = 0
var int   nLvlMerged  = 0
var int   nEvBull     = 0
var int   nEvBear     = 0

var array<int>   kN   = array.new<int>(3, 0)
var array<int>   kW   = array.new<int>(3, 0)
var array<int>   kTO  = array.new<int>(3, 0)
var array<float> kBar = array.new<float>(3, 0.0)
var array<float> kMfe = array.new<float>(3, 0.0)
var array<float> kMae = array.new<float>(3, 0.0)
var array<int>   sN   = array.new<int>(2, 0)
var array<int>   sW   = array.new<int>(2, 0)
var array<int>   bN   = array.new<int>(3, 0)
var array<int>   bW   = array.new<int>(3, 0)
var int   totN  = 0
var int   totW  = 0
var int   totTO = 0
var float totR  = 0.0
var float sumWinR  = 0.0
var float sumLossR = 0.0
var float eqPeak = 0.0
var float maxDD  = 0.0
var int   streak = 0
var int   lastSigBar  = na
var int   lastSigDir  = 0
var int   lastSigKind = 0
var float lastSigSc   = na

bool evXBull = false
bool evXBear = false
bool evSfpBull = false
bool evSfpBear = false
bool evMssBull = false
bool evMssBear = false
bool sigNow = false
bool sigStrongNow = false
bool outTgtNow = false
bool outInvNow = false
bool newLvlBull = false
bool newLvlBear = false
float bestSigSc = 0.0

f_recycleHist() =>
    while hLines.size() > maxHistInput * 2
        line.delete(hLines.shift())
    while hLbls.size() > maxHistInput * 2
        label.delete(hLbls.shift())
    while hBoxes.size() > maxHistInput
        box.delete(hBoxes.shift())
    while outLbls.size() > maxHistInput * 2
        label.delete(outLbls.shift())

f_tagText(Lvl L) =>
    f_px(L.price) + " " + f_stars(L.score) + (L.touches > 1 ? " ×" + str.tostring(L.touches) : "") + " " + f_sessShort(L.sess)

f_tagTip(Lvl L) =>
    (L.dir == 1 ? "SELL-SIDE liquidity · stops above a swing high" : "BUY-SIDE liquidity · stops below a swing low") +
     "\nOrigin " + f_sessName(L.sess) + " session · " + str.tostring(L.touches) + " touch" + (L.touches > 1 ? "es (equal level pool)" : "") +
     "\nLevel strength " + str.tostring(L.score, "#.0") + "/10 " + f_stars(L.score) +
     "\nAge " + str.tostring(bar_index - L.pbar) + " bars" + (L.pierced ? "\n● PIERCED · waiting for reclaim (SFP) or acceptance (MSS)" : "")

f_refreshTag(Lvl L) =>
    if not na(L.tag)
        label.set_text(L.tag, f_tagText(L))
        label.set_tooltip(L.tag, f_tagTip(L))
        label.set_x(L.tag, bar_index + extInput)
        if L.pierced
            label.set_color(L.tag, color.new(f_sessCol(L.sess), 35))

f_levelScore(int _sess, int _touch, float _vr, float _wick) =>
    float s = 2.0 + f_sessW(_sess) + math.min(3.0, (_touch - 1) * 1.5) + f_clamp(_vr - 1.0, 0.0, 1.5) + f_clamp(_wick, 0.0, 1.0) * 2.0
    f_clamp(s, 0.0, 10.0)

f_createLevel(int _dir, float _price) =>
    int pbar = pivBar
    int ps   = pivSess
    float vr = pivVol
    float wk = _dir == 1 ? pivWickUp : pivWickDn
    color sc = f_sessCol(ps)
    line  gl = na
    if glowInput
        gl := line.new(pbar, _price, bar_index + extInput, _price, xloc=xloc.bar_index, color=color.new(sc, GLOW_T), style=line.style_solid, width=widthInput + 4)
    line  ln = line.new(pbar, _price, bar_index + extInput, _price, xloc=xloc.bar_index, color=sc, style=line.style_solid, width=widthInput)
    label tg = na
    int   noBar = na
    Lvl L = Lvl.new(_dir, _price, pbar, ps, 1, f_levelScore(ps, 1, vr, wk), false, noBar, _price, ln, gl, tg)
    if showPriceInput
        tg := label.new(bar_index + extInput, _price, "", xloc=xloc.bar_index, yloc=yloc.price, color=sc, style=label.style_label_left, textcolor=f_textOn(sc), size=f_size3(tagSizeInput))
        L.tag := tg
        f_refreshTag(L)
    L

f_countSide(int _dir) =>
    int c = 0
    if lvls.size() > 0
        for i = 0 to lvls.size() - 1
            if lvls.get(i).dir == _dir
                c += 1
    c

f_dropLevel(int i) =>
    Lvl L = lvls.get(i)
    line.delete(L.ln)
    if not na(L.glow)
        line.delete(L.glow)
    if not na(L.tag)
        label.delete(L.tag)
    lvls.remove(i)

f_expireOldest(int _dir) =>

    if lvls.size() > 0
        int idx = -1
        for i = 0 to lvls.size() - 1
            if idx < 0 and lvls.get(i).dir == _dir
                idx := i
        if idx >= 0
            f_dropLevel(idx)

f_tryMerge(int _dir, float _price) =>

    bool merged = false
    if eqMergeInput and lvls.size() > 0
        for i = 0 to lvls.size() - 1
            Lvl L = lvls.get(i)
            if not merged and L.dir == _dir and not L.pierced and math.abs(L.price - _price) <= eqTolInput * atrSafe
                L.touches += 1
                float vr = pivVol
                float wk = _dir == 1 ? pivWickUp : pivWickDn
                L.score := math.max(L.score, f_levelScore(L.sess, L.touches, vr, wk))

                if _dir == 1 and _price > L.price
                    L.price := _price
                else if _dir == -1 and _price < L.price
                    L.price := _price
                L.extreme := L.price
                line.set_y1(L.ln, L.price)
                line.set_y2(L.ln, L.price)
                if not na(L.glow)
                    line.set_y1(L.glow, L.price)
                    line.set_y2(L.glow, L.price)
                if not na(L.tag)
                    label.set_y(L.tag, L.price)
                f_refreshTag(L)
                merged := true
    merged

if not na(ph)
    bool broken = false
    for i = 0 to prdInput - 1
        if high[i] > ph
            broken := true
    if not broken
        if f_tryMerge(1, ph)
            nLvlMerged += 1
        else
            if f_countSide(1) >= maxActInput
                f_expireOldest(1)
                nLvlExpired += 1
            Lvl L = f_createLevel(1, ph)
            lvls.push(L)
            nLvlCreated += 1
            sesLvl.set(L.sess, sesLvl.get(L.sess) + 1)
            newLvlBear := true

if not na(pl)
    bool broken = false
    for i = 0 to prdInput - 1
        if low[i] < pl
            broken := true
    if not broken
        if f_tryMerge(-1, pl)
            nLvlMerged += 1
        else
            if f_countSide(-1) >= maxActInput
                f_expireOldest(-1)
                nLvlExpired += 1
            Lvl L = f_createLevel(-1, pl)
            lvls.push(L)
            nLvlCreated += 1
            sesLvl.set(L.sess, sesLvl.get(L.sess) + 1)
            newLvlBull := true

f_sigStrength(Lvl L, int kind, int thesis, float depthAtr) =>

    float base  = L.score * 0.4
    float depth = f_clamp(depthAtr, 0.0, 1.0) * 2.0
    float vol   = hasVol ? f_clamp(volRatio - 1.0, 0.0, 1.0) * 2.0 : 1.0
    float body  = kind == 2 ? f_clamp(math.abs(close - close[1]) / atrSafe, 0.0, 0.5) * 4.0 : f_clamp(math.abs(close - L.price) / atrSafe, 0.0, 0.5) * 4.0
    f_clamp(base + depth + vol + body, 0.0, 10.0)

f_fireEvent(Lvl L, int kind, int thesis, float depthAtr, float sc) =>
    color kc = f_kindCol(kind)
    color ac = thesis == 1 ? bullAccent : bearAccent
    string ks = f_kindName(kind)
    line.set_x2(L.ln, bar_index)
    line.set_color(L.ln, kc)
    line.set_style(L.ln, f_kindStyle(kind))
    line.set_width(L.ln, kind == 2 ? widthInput + 1 : widthInput)
    if not na(L.glow)
        line.set_x2(L.glow, bar_index)
        line.set_color(L.glow, color.new(kc, GLOW_T))
    if not na(L.tag)
        label.delete(L.tag)
    string tip = ks + " · " + (kind == 0 ? "liquidity sweep · wick through the pool, same-bar close back inside (stop raid, no acceptance)" : kind == 1 ? "closed through, then reclaimed (swing failure)" : "closed through and accepted (structure shift)") +
         "\nThesis " + (thesis == 1 ? "BULLISH ▲" : "BEARISH ▼") + " · " + f_tierWord(sc) + " " + str.tostring(sc, "#.0") + "/10 " + f_stars(sc) +
         "\nLevel " + f_px(L.price) + " · " + (L.dir == 1 ? "sell-side" : "buy-side") + " · origin " + f_sessName(L.sess) + " · " + str.tostring(L.touches) + " touch" +
         "\nSweep depth " + str.tostring(depthAtr, "#.00") + " ATR · level age " + str.tostring(bar_index - L.pbar) + " bars" +
         "\nVolume " + (hasVol ? str.tostring(volRatio, "#.00") + "× avg" : "n/a") + " · event session " + f_sessName(sessNow)
    if f_kindShow(kind)
        int   midX = math.round((L.pbar + bar_index) / 2.0)
        float mult = L.dir == 1 ? (flipInput ? -1.0 : 1.0) : (flipInput ? 1.0 : -1.0)
        label ml = label.new(midX, L.price + offAtrInput * atrSafe * mult, ks, xloc=xloc.bar_index, yloc=yloc.price,
             color=color.new(kc, 100), style=label.style_none, textcolor=kc, size=f_size5(kindSizeInput), tooltip=tip)
        hLbls.push(ml)
        if showSweepBox and kind != 2 and not na(L.extreme) and L.extreme != L.price
            int bx1 = L.pierced ? L.pierceBar : bar_index
            float top = L.dir == 1 ? L.extreme : L.price
            float bot = L.dir == 1 ? L.price : L.extreme
            box bx = box.new(bx1, top, bar_index, bot, border_color=color.new(ac, BOX_BRD), border_width=1, border_style=line.style_dotted,
                 xloc=xloc.bar_index, bgcolor=color.new(ac, BOX_T))
            hBoxes.push(bx)
    if showSigLblInput and sc >= minStrInput
        label sl = label.new(bar_index, thesis == 1 ? low : high, ks + " " + f_stars(sc) + " " + str.tostring(sc, "#.0"),
             xloc=xloc.bar_index, yloc=yloc.price, color=color.new(ac, sc >= 7.5 ? 0 : sc >= 5.0 ? 15 : 35),
             style=thesis == 1 ? label.style_label_up : label.style_label_down, textcolor=f_textOn(ac), size=f_size3(tagSizeInput), tooltip=tip)
        hLbls.push(sl)
    hLines.push(L.ln)
    if not na(L.glow)
        hLines.push(L.glow)
    f_recycleHist()

f_openSig(Lvl L, int kind, int thesis, float sc) =>
    float riskPx = invUInput * atrSafe
    if stopModeInput == "Structural"
        float structDist = kind == 2 ? math.abs(close - L.price) : math.abs(close - L.extreme)
        riskPx := structDist + bufInput * atrSafe
    riskPx := math.max(riskPx, minRiskInput * atrSafe)
    float tgt = close + thesis * tgtRInput * riskPx
    float inv = close - thesis * riskPx
    line noLn = na
    Sig s = Sig.new(thesis, kind, sessNow, f_bucket(sc), close, tgt, inv, riskPx, bar_index, 0.0, 0.0, noLn, noLn)
    if showTgtInput
        s.lnT := line.new(bar_index, tgt, bar_index + 1, tgt, xloc=xloc.bar_index, color=color.new(thesis == 1 ? bullAccent : bearAccent, 30), style=line.style_dotted, width=1)
        s.lnX := line.new(bar_index, inv, bar_index + 1, inv, xloc=xloc.bar_index, color=color.new(thesis == 1 ? bearAccent : bullAccent, 30), style=line.style_dotted, width=1)
    sigs.push(s)
    s

if barstate.isconfirmed and lvls.size() > 0
    for i = lvls.size() - 1 to 0
        Lvl L = lvls.get(i)
        int   d   = L.dir
        float lvl = L.price
        bool  beyond      = d == 1 ? high >= lvl : low <= lvl
        bool  closeBeyond = d == 1 ? close > lvl : close < lvl
        bool  closeInside = d == 1 ? close < lvl : close > lvl
        float wickExt     = d == 1 ? high : low
        int   kind = -1

        if maxAgeInput > 0 and bar_index - L.pbar > maxAgeInput
            f_dropLevel(i)
            nLvlExpired += 1
        else
            if L.pierced
                L.extreme := d == 1 ? math.max(L.extreme, wickExt) : math.min(L.extreme, wickExt)
                bool momentum = d == 1 ? close > close[1] : close < close[1]
                bool dispOk   = mssDispInput <= 0.0 or math.abs(close - lvl) >= mssDispInput * atrSafe
                if closeInside
                    kind := 1
                else if closeBeyond and momentum and dispOk
                    kind := 2
                else if bar_index - L.pierceBar >= patienceInput and closeBeyond
                    kind := 2
                if kind < 0
                    line.set_x2(L.ln, bar_index + extInput)
                    if not na(L.glow)
                        line.set_x2(L.glow, bar_index + extInput)
                    f_refreshTag(L)
            else
                if beyond and closeInside
                    L.extreme := wickExt
                    kind := 0
                else if closeBeyond
                    L.pierced := true
                    L.pierceBar := bar_index
                    L.extreme := wickExt
                    f_refreshTag(L)
                else

                    line.set_x2(L.ln, bar_index + extInput)
                    if not na(L.glow)
                        line.set_x2(L.glow, bar_index + extInput)
                    f_refreshTag(L)
            if kind >= 0
                int   thesis   = kind == 2 ? d : -d
                float depthAtr = math.abs(L.extreme - lvl) / atrSafe
                float sc       = f_sigStrength(L, kind, thesis, depthAtr)
                f_fireEvent(L, kind, thesis, depthAtr, sc)

                evKind.set(kind, evKind.get(kind) + 1)
                evDepth.set(kind, evDepth.get(kind) + depthAtr)
                evAge.set(kind, evAge.get(kind) + (bar_index - L.pbar))
                sesEv.set(L.sess * 3 + kind, sesEv.get(L.sess * 3 + kind) + 1)
                nEvBull += thesis == 1 ? 1 : 0
                nEvBear += thesis == -1 ? 1 : 0
                if kind == 0
                    evXBull := evXBull or thesis == 1
                    evXBear := evXBear or thesis == -1
                else if kind == 1
                    evSfpBull := evSfpBull or thesis == 1
                    evSfpBear := evSfpBear or thesis == -1
                else
                    evMssBull := evMssBull or thesis == 1
                    evMssBear := evMssBear or thesis == -1
                if sc >= minStrInput
                    f_openSig(L, kind, thesis, sc)
                    sigNow := true
                    sigStrongNow := sigStrongNow or sc >= 7.5
                    bestSigSc := math.max(bestSigSc, sc)
                    lastSigBar := bar_index
                    lastSigDir := thesis
                    lastSigKind := kind
                    lastSigSc := sc
                lvls.remove(i)

if sigs.size() > 0
    for i = sigs.size() - 1 to 0
        Sig s = sigs.get(i)
        if bar_index > s.ebar
            float toward  = s.dir == 1 ? (high - s.entry) / s.risk : (s.entry - low) / s.risk
            float against = s.dir == 1 ? (s.entry - low) / s.risk : (high - s.entry) / s.risk
            s.mfe := math.max(s.mfe, toward)
            s.mae := math.max(s.mae, against)
            bool hitT = s.dir == 1 ? high >= s.target  : low <= s.target
            bool hitX = s.dir == 1 ? low <= s.invalid  : high >= s.invalid
            bool done = false
            int  res  = 0
            float rr  = 0.0
            if hitX
                done := true
                res := 2
                rr := -1.0
            else if hitT
                done := true
                res := 1
                rr := tgtRInput
            else if bar_index - s.ebar >= maxHoldInput
                done := true
                res := 3
                rr := (close - s.entry) * s.dir / s.risk
            if done
                int  sd  = s.dir == 1 ? 0 : 1
                bool win = res == 1
                kN.set(s.kind, kN.get(s.kind) + 1)
                sN.set(sd, sN.get(sd) + 1)
                bN.set(s.bucket, bN.get(s.bucket) + 1)
                sesSigN.set(s.sess, sesSigN.get(s.sess) + 1)
                kMfe.set(s.kind, kMfe.get(s.kind) + s.mfe)
                kMae.set(s.kind, kMae.get(s.kind) + s.mae)
                totN += 1
                totR += rr
                if win
                    kW.set(s.kind, kW.get(s.kind) + 1)
                    sW.set(sd, sW.get(sd) + 1)
                    bW.set(s.bucket, bW.get(s.bucket) + 1)
                    sesSigW.set(s.sess, sesSigW.get(s.sess) + 1)
                    kBar.set(s.kind, kBar.get(s.kind) + (bar_index - s.ebar))
                    totW += 1
                    sumWinR += rr
                else
                    sumLossR += -math.min(rr, 0.0)
                if res == 3
                    kTO.set(s.kind, kTO.get(s.kind) + 1)
                    totTO += 1
                eqPeak := math.max(eqPeak, totR)
                maxDD  := math.max(maxDD, eqPeak - totR)
                streak := win ? (streak > 0 ? streak + 1 : 1) : (streak < 0 ? streak - 1 : -1)
                outTgtNow := outTgtNow or res == 1
                outInvNow := outInvNow or res == 2
                if not na(s.lnT)
                    line.delete(s.lnT)
                if not na(s.lnX)
                    line.delete(s.lnX)
                if showOutInput
                    float exPx = res == 1 ? s.target : res == 2 ? s.invalid : close
                    color xc   = res == 1 ? (s.dir == 1 ? bullAccent : bearAccent) : res == 2 ? (s.dir == 1 ? bearAccent : bullAccent) : MUTED
                    string ek  = res == 1 ? "✓ TGT " : res == 2 ? "✕ INV " : "◦ END "
                    outLbls.push(label.new(bar_index, exPx, ek + f_sn(rr, "#.0") + "R", xloc=xloc.bar_index, yloc=yloc.price,
                         style=label.style_label_left, color=color.new(xc, 82), textcolor=xc, size=size.tiny,
                         tooltip=f_kindName(s.kind) + " " + (s.dir == 1 ? "long" : "short") + " · " + (res == 1 ? "target reached" : res == 2 ? "invalidated" : "timed out after " + str.tostring(maxHoldInput) + " bars") +
                         "\nResult " + f_sn(rr, "#.00") + "R · " + str.tostring(bar_index - s.ebar) + " bars\nMFE " + str.tostring(s.mfe, "#.00") + "R · MAE " + str.tostring(s.mae, "#.00") + "R"))
                    f_recycleHist()
                sigs.remove(i)
            else if showTgtInput and not na(s.lnT)
                line.set_x2(s.lnT, bar_index)
                line.set_x2(s.lnX, bar_index)

f_keyLine(line _ln, label _lb, float _y, string _nm, color _c, bool _swept) =>
    line  ln = _ln
    label lb = _lb
    if showKeyInput and not na(_y)
        color cc = _swept ? color.new(_c, 60) : _c
        int   x1 = math.max(0, bar_index - keyLenInput)
        if na(ln)
            ln := line.new(x1, _y, bar_index + extInput, _y, xloc=xloc.bar_index, color=cc, style=line.style_dashed, width=1)
            lb := label.new(bar_index + extInput, _y, _nm, xloc=xloc.bar_index, yloc=yloc.price, color=color.new(cc, 100), style=label.style_label_left, textcolor=cc, size=size.tiny)
        else
            line.set_xy1(ln, x1, _y)
            line.set_xy2(ln, bar_index + extInput, _y)
            line.set_color(ln, cc)
            label.set_xy(lb, bar_index + extInput, _y)
            label.set_text(lb, _nm + (_swept ? " ●" : ""))
            label.set_textcolor(lb, cc)
    else
        if not na(ln)
            line.delete(ln)
            label.delete(lb)
            ln := na
            lb := na
    [ln, lb]

[pdhLn2, pdhLb2] = f_keyLine(pdhLn, pdhLb, pdH, "PDH", bearSoft, pdhSwept)
[pdlLn2, pdlLb2] = f_keyLine(pdlLn, pdlLb, pdL, "PDL", bullSoft, pdlSwept)
[pwhLn2, pwhLb2] = f_keyLine(pwhLn, pwhLb, pwH, "PWH", bearAccent, pwhSwept)
[pwlLn2, pwlLb2] = f_keyLine(pwlLn, pwlLb, pwL, "PWL", bullAccent, pwlSwept)
pdhLn := pdhLn2
pdhLb := pdhLb2
pdlLn := pdlLn2
pdlLb := pdlLb2
pwhLn := pwhLn2
pwhLb := pwhLb2
pwlLn := pwlLn2
pwlLb := pwlLb2

float nearUp = na
float nearDn = na
string nearUpTag = ""
string nearDnTag = ""
int actBear = 0
int actBull = 0
float sumBearScore = 0.0
float sumBullScore = 0.0
if lvls.size() > 0
    for i = 0 to lvls.size() - 1
        Lvl L = lvls.get(i)
        if L.dir == 1
            actBear += 1
            sumBearScore += L.score
            if L.price > close and (na(nearUp) or L.price < nearUp)
                nearUp := L.price
                nearUpTag := f_sessShort(L.sess) + " " + f_stars(L.score)
        else
            actBull += 1
            sumBullScore += L.score
            if L.price < close and (na(nearDn) or L.price > nearDn)
                nearDn := L.price
                nearDnTag := f_sessShort(L.sess) + " " + f_stars(L.score)
if not na(pdH) and not pdhSwept and pdH > close and (na(nearUp) or pdH < nearUp)
    nearUp := pdH
    nearUpTag := "PDH"
if not na(pwH) and not pwhSwept and pwH > close and (na(nearUp) or pwH < nearUp)
    nearUp := pwH
    nearUpTag := "PWH"
if not na(pdL) and not pdlSwept and pdL < close and (na(nearDn) or pdL > nearDn)
    nearDn := pdL
    nearDnTag := "PDL"
if not na(pwL) and not pwlSwept and pwL < close and (na(nearDn) or pwL > nearDn)
    nearDn := pwL
    nearDnTag := "PWL"
float distUp = na(nearUp) ? na : (nearUp - close) / atrSafe
float distDn = na(nearDn) ? na : (close - nearDn) / atrSafe
bool nearUpNow = not na(distUp) and distUp <= proxInput and (na(distUp[1]) or distUp[1] > proxInput)
bool nearDnNow = not na(distDn) and distDn <= proxInput and (na(distDn[1]) or distDn[1] > proxInput)

plot(nearUp, title="Nearest Sell-Side Liquidity", color=CLEAR, display=display.data_window)
plot(nearDn, title="Nearest Buy-Side Liquidity",  color=CLEAR, display=display.data_window)
plot(bestSigSc * (lastSigDir == 0 ? 0 : lastSigDir), title="Signal Strength (+long / −short)", color=CLEAR, display=display.data_window)

string dSize = dashSizeS == "Tiny" ? size.tiny : dashSizeS == "Normal" ? size.normal : dashSizeS == "Large" ? size.large : size.small
string dFont = monoFont ? font.family_monospace : font.family_default
string dPos  = switch dashPos
    "Top Right"    => position.top_right
    "Middle Right" => position.middle_right
    "Bottom Right" => position.bottom_right
    "Top Left"     => position.top_left
    "Middle Left"  => position.middle_left
    => position.bottom_left

var table dash = table.new(dPos, 9, 80, bgcolor=CLEAR, frame_width=0, border_width=0)

f_c(int c, int r, string tx, color tc, color bg, string ha, string tip) =>
    table.cell(dash, c, r, tx, text_color=tc, bgcolor=bg, text_halign=ha, text_size=dSize, text_font_family=dFont, tooltip=tip)

f_strip(int r, color ac) =>
    table.cell(dash, 0, r, "", bgcolor=ac, width=0.3)

f_sep(int r) =>
    for c = 0 to 8
        table.cell(dash, c, r, "", bgcolor=CLEAR, height=sepH)
    table.merge_cells(dash, 0, r, 8, r)

f_head(int r, string title, string sub, color ac) =>
    f_strip(r, ac)
    f_c(1, r, title, TXT, HEAD, text.align_left, "")
    table.merge_cells(dash, 1, r, 5, r)
    f_c(6, r, sub, ac, HEAD, text.align_right, "")
    table.merge_cells(dash, 6, r, 8, r)

f_kv(int r, string k, string v, color vc, color ac, string tip) =>
    color bg = r % 2 == 0 ? PANEL : PANEL2
    f_strip(r, color.new(ac, 55))
    f_c(1, r, k, MUTED, bg, text.align_left, "")
    table.merge_cells(dash, 1, r, 3, r)
    f_c(4, r, v, vc, bg, text.align_right, tip)
    table.merge_cells(dash, 4, r, 8, r)

f_colhd(int r, array<string> hd, color ac) =>
    f_strip(r, color.new(ac, 55))
    for c = 1 to 8
        f_c(c, r, hd.get(c - 1), MUTED, COLHD, c == 1 ? text.align_left : text.align_right, "")

f_note(int r, string tx, color ac) =>
    f_strip(r, color.new(ac, 55))
    f_c(1, r, tx, DIM, PANEL, text.align_center, "")
    table.merge_cells(dash, 1, r, 8, r)

f_priceRow(int r, color ac) =>
    f_strip(r, ac)
    f_c(1, r, "PRICE ◀", WHITE, PRICEBG, text.align_left, "")
    f_c(2, r, f_px(close), WHITE, PRICEBG, text.align_right, "")
    f_c(3, r, "ATR " + f_px(atrUnit), MUTED, PRICEBG, text.align_left, "")
    table.merge_cells(dash, 3, r, 5, r)
    f_c(6, r, "▲ " + (na(distUp) ? "—" : str.tostring(distUp, "#.0") + "a") + " · ▼ " + (na(distDn) ? "—" : str.tostring(distDn, "#.0") + "a"), TXT, PRICEBG, text.align_right, "Distance to the nearest liquidity above / below in ATR")
    table.merge_cells(dash, 6, r, 8, r)

f_mapRow(int r, string nm, float px, int sess, float sc, int age, int touch, bool pierced, bool isKey, bool swept, color lc, color ac) =>
    color  bg   = r % 2 == 0 ? PANEL : PANEL2
    float  dist = (px - close) / atrSafe
    color  nc   = swept ? color.new(lc, 55) : lc
    f_strip(r, pierced ? GOLD : color.new(ac, 55))
    f_c(1, r, nm + (pierced ? " ●" : swept ? " ○" : ""), nc, bg, text.align_left, isKey ? "Key liquidity from the last closed period · ● swept this period" : (pierced ? "PIERCED · closed through, waiting for SFP reclaim or MSS acceptance" : "Active pivot liquidity pool"))
    f_c(2, r, f_px(px), swept ? MUTED : TXT, bg, text.align_right, "")
    f_c(3, r, f_sn(dist, "#.00"), math.abs(dist) <= proxInput ? GOLD : MUTED, bg, text.align_right, "Distance from price in ATR · gold = inside the proximity window")
    f_c(4, r, isKey ? "KEY" : f_sessShort(sess), isKey ? WHITE : f_sessCol(sess), bg, text.align_right, isKey ? "" : "Origin session " + f_sessName(sess))
    f_c(5, r, isKey ? "—" : str.tostring(sc, "#.0"), isKey ? DIM : sc >= 7.5 ? GOLD : sc >= 5.0 ? TXT : MUTED, bg, text.align_right, "Level strength 0–10 · session weight + equal-level touches + pivot volume + rejection wick")
    f_c(6, r, isKey ? "—" : str.tostring(age), isKey ? DIM : MUTED, bg, text.align_right, "Bars since the pivot")
    f_c(7, r, isKey ? "—" : touch > 1 ? "×" + str.tostring(touch) : "1", isKey ? DIM : touch > 1 ? GOLD : MUTED, bg, text.align_right, "Equal-level touches merged into this pool")
    f_c(8, r, isKey ? (swept ? "SWEPT" : "INTACT") : f_stars(sc), isKey ? (swept ? MUTED : nc) : color.new(lc, 10), bg, text.align_right, "")

f_arow(int r, string nm, int n, int w, int nTo, float sumB, float sumMfe, float sumMae, color lc, color ac, string tip) =>
    color  bg = r % 2 == 0 ? PANEL : PANEL2
    float  wr = n > 0 ? float(w) / n * 100.0 : 0.0
    float  be = tgtRInput > 0 ? 100.0 / (1.0 + tgtRInput) : 50.0
    color  wc = n == 0 ? DIM : wr >= be + 5.0 ? bullAccent : wr <= be - 5.0 ? bearAccent : TXT
    f_strip(r, color.new(ac, 55))
    f_c(1, r, nm, n == 0 ? DIM : lc, bg, text.align_left, tip)
    table.merge_cells(dash, 1, r, 2, r)
    f_c(3, r, str.tostring(n), n == 0 ? DIM : TXT, bg, text.align_right, "Resolved signals")
    f_c(4, r, n > 0 ? str.tostring(wr, "#.0") + "%" : "—", wc, bg, text.align_right, "Target reached before invalidation · breakeven at " + str.tostring(be, "#") + "% for " + str.tostring(tgtRInput, "#.##") + "R")
    f_c(5, r, n > 0 ? str.tostring(nTo) : "—", n == 0 ? DIM : nTo > 0 ? GOLD : MUTED, bg, text.align_right, "Timeouts (counted as non-wins)")
    f_c(6, r, w > 0 ? str.tostring(sumB / w, "#.0") : "—", n == 0 ? DIM : TXT, bg, text.align_right, "Average bars to target (winning signals)")
    f_c(7, r, n > 0 ? str.tostring(sumMfe / n, "#.00") + "/" + str.tostring(sumMae / n, "#.00") : "—", n == 0 ? DIM : TXT, bg, text.align_right, "Average MFE / MAE in R")
    f_c(8, r, n > 0 ? f_gauge(wr / 100.0, 5) : "", wc, bg, text.align_left, "")

f_srow(int r, int s, color ac) =>
    color  bg  = r % 2 == 0 ? PANEL : PANEL2
    color  sc  = f_sessCol(s)
    int    nl  = sesLvl.get(s)
    int    nx  = sesEv.get(s * 3)
    int    nf  = sesEv.get(s * 3 + 1)
    int    nm  = sesEv.get(s * 3 + 2)
    int    ne  = nx + nf + nm
    int    gn  = sesSigN.get(s)
    int    gw  = sesSigW.get(s)
    float  wr  = gn > 0 ? float(gw) / gn * 100.0 : 0.0
    float  be  = tgtRInput > 0 ? 100.0 / (1.0 + tgtRInput) : 50.0
    f_strip(r, color.new(sc, 30))
    f_c(1, r, (s == sessNow ? "● " : "  ") + f_sessName(s), s == sessNow ? sc : color.new(sc, 25), bg, text.align_left, s == sessNow ? "Current session" : "")
    f_c(2, r, str.tostring(nl), nl == 0 ? DIM : TXT, bg, text.align_right, "Liquidity levels born in this session")
    f_c(3, r, ne > 0 ? f_pct(nx, ne) : "—", ne == 0 ? DIM : colXInput, bg, text.align_right, "Share of this session's levels resolved as SWEEP (wick raid)")
    f_c(4, r, ne > 0 ? f_pct(nf, ne) : "—", ne == 0 ? DIM : colSfpInput, bg, text.align_right, "Share resolved as SFP (reclaim)")
    f_c(5, r, ne > 0 ? f_pct(nm, ne) : "—", ne == 0 ? DIM : colMssInput, bg, text.align_right, "Share resolved as MSS (structure shift)")
    f_c(6, r, str.tostring(gn), gn == 0 ? DIM : TXT, bg, text.align_right, "Resolved signals fired DURING this session")
    f_c(7, r, gn > 0 ? str.tostring(wr, "#") + "%" : "—", gn == 0 ? DIM : wr >= be + 5.0 ? bullAccent : wr <= be - 5.0 ? bearAccent : TXT, bg, text.align_right, "WIN% of signals fired during this session")
    f_c(8, r, gn > 0 ? f_gauge(wr / 100.0, 5) : "", gn == 0 ? DIM : TXT, bg, text.align_left, "")

f_biasTxt(float b) => na(b) ? "—" : b >= 1.0 ? "▲▲" : b >= 0.25 ? "▲" : b <= -1.0 ? "▼▼" : b <= -0.25 ? "▼" : "·"
f_biasCol(float b) => na(b) ? DIM : b >= 0.25 ? bullAccent : b <= -0.25 ? bearAccent : MUTED

if barstate.islast
    dash.clear(0, 0, 8, 79)
    if dashOn
        int  r       = 0
        bool needSep = false

        if showP1
            color ac = GOLD
            f_head(r, "◆ SFP MSS · LIQUIDITY STATE", syminfo.ticker + " · " + f_prettyTf(timeframe.period), ac)
            r += 1
            f_kv(r, "Session", "● " + f_sessName(sessNow) + " · " + tzInput, f_sessCol(sessNow), ac, "Session windows are read in the selected timezone")
            r += 1
            f_kv(r, "Active Pools", "▲ " + str.tostring(actBear) + " sell-side " + f_gauge(f_frac(actBear, maxActInput), 5) + "  ▼ " + str.tostring(actBull) + " buy-side " + f_gauge(f_frac(actBull, maxActInput), 5), TXT, ac, "Unswept pivot liquidity per side · cap " + str.tostring(maxActInput))
            r += 1
            f_kv(r, "Nearest ▲ Target", na(nearUp) ? "—" : f_px(nearUp) + " · " + str.tostring(distUp, "#.00") + " ATR · " + nearUpTag, na(nearUp) ? MUTED : distUp <= proxInput ? GOLD : bearSoft, ac, "Closest sell-side liquidity above price (pivot pool or intact key level)")
            r += 1
            f_kv(r, "Nearest ▼ Target", na(nearDn) ? "—" : f_px(nearDn) + " · " + str.tostring(distDn, "#.00") + " ATR · " + nearDnTag, na(nearDn) ? MUTED : distDn <= proxInput ? GOLD : bullSoft, ac, "Closest buy-side liquidity below price (pivot pool or intact key level)")
            r += 1
            float avgBear = actBear > 0 ? sumBearScore / actBear : 0.0
            float avgBull = actBull > 0 ? sumBullScore / actBull : 0.0
            string magS = actBear == 0 and actBull == 0 ? "—" : avgBear > avgBull + 0.5 ? "▲ SELL-SIDE heavier · " + str.tostring(avgBear, "#.0") + " vs " + str.tostring(avgBull, "#.0") : avgBull > avgBear + 0.5 ? "▼ BUY-SIDE heavier · " + str.tostring(avgBull, "#.0") + " vs " + str.tostring(avgBear, "#.0") : "◆ BALANCED · " + str.tostring(avgBear, "#.0") + " / " + str.tostring(avgBull, "#.0")
            f_kv(r, "Liquidity Magnet", magS, avgBear > avgBull + 0.5 ? bearAccent : avgBull > avgBear + 0.5 ? bullAccent : TXT, ac, "Average strength of the active pools on each side · the heavier side is the more attractive draw on liquidity")
            r += 1
            f_kv(r, "MTF Bias", f_prettyTf(timeframe.period) + " " + f_biasTxt(biasRaw) + (tf1 == "" ? "" : " · " + f_prettyTf(tf1) + " " + f_biasTxt(b1)) + (tf2 == "" ? "" : " · " + f_prettyTf(tf2) + " " + f_biasTxt(b2)) + (tf3 == "" ? "" : " · " + f_prettyTf(tf3) + " " + f_biasTxt(b3)), f_biasCol(biasRaw), ac, "Close vs EMA50 in ATR · chart TF live, higher timeframes from their last closed bar")
            r += 1
            string flowS = nEvBull + nEvBear == 0 ? "—" : "▲ " + str.tostring(nEvBull) + " bullish · ▼ " + str.tostring(nEvBear) + " bearish theses"
            f_kv(r, "Sweep Flow", flowS, nEvBull > nEvBear ? bullAccent : nEvBear > nEvBull ? bearAccent : TXT, ac, "Direction of every classified resolution so far · SWEEP and SFP are reversal theses, MSS is continuation")
            r += 1
            string ls = na(lastSigBar) ? "—" : f_kindName(lastSigKind) + (lastSigDir == 1 ? " ▲ " : " ▼ ") + str.tostring(lastSigSc, "#.0") + " " + f_stars(lastSigSc) + " · " + str.tostring(bar_index - lastSigBar) + " bars ago"
            f_kv(r, "Last Signal · Open", ls + " · " + str.tostring(sigs.size()) + " open", na(lastSigBar) ? MUTED : lastSigDir == 1 ? bullAccent : bearAccent, ac, "")
            r += 1
            needSep := true

        if showP2
            if needSep
                f_sep(r)
                r += 1
            color ac = WHITE
            f_head(r, "◆ LIQUIDITY MAP", str.tostring(lvls.size()) + " pools", ac)
            r += 1
            f_colhd(r, array.from("LEVEL", "PRICE", "ATR", "SESS", "STR", "AGE", "EQ", "RANK"), ac)
            r += 1

            array<float> mpPx   = array.new<float>()
            array<int>   mpIdx  = array.new<int>()
            if lvls.size() > 0
                for i = 0 to lvls.size() - 1
                    mpPx.push(lvls.get(i).price)
                    mpIdx.push(i)
            if showKeyInput
                if not na(pdH)
                    mpPx.push(pdH)
                    mpIdx.push(-1)
                if not na(pdL)
                    mpPx.push(pdL)
                    mpIdx.push(-2)
                if not na(pwH)
                    mpPx.push(pwH)
                    mpIdx.push(-3)
                if not na(pwL)
                    mpPx.push(pwL)
                    mpIdx.push(-4)
            if mpPx.size() == 0
                f_note(r, "no liquidity mapped yet · waiting for confirmed pivots", ac)
                r += 1
            else
                array<int> ord = mpPx.sort_indices(order.descending)
                bool priceDone = false
                for j = 0 to ord.size() - 1
                    int   oi = ord.get(j)
                    float px = mpPx.get(oi)
                    int   ix = mpIdx.get(oi)
                    if not priceDone and close >= px
                        f_priceRow(r, ac)
                        r += 1
                        priceDone := true
                    if ix >= 0
                        Lvl L = lvls.get(ix)
                        f_mapRow(r, (L.dir == 1 ? "SSL ▲" : "BSL ▼"), L.price, L.sess, L.score, bar_index - L.pbar, L.touches, L.pierced, false, false, L.dir == 1 ? bearSoft : bullSoft, ac)
                    else if ix == -1
                        f_mapRow(r, "PDH", pdH, 0, 0.0, 0, 0, false, true, pdhSwept, bearSoft, ac)
                    else if ix == -2
                        f_mapRow(r, "PDL", pdL, 0, 0.0, 0, 0, false, true, pdlSwept, bullSoft, ac)
                    else if ix == -3
                        f_mapRow(r, "PWH", pwH, 0, 0.0, 0, 0, false, true, pwhSwept, bearAccent, ac)
                    else
                        f_mapRow(r, "PWL", pwL, 0, 0.0, 0, 0, false, true, pwlSwept, bullAccent, ac)
                    r += 1
                if not priceDone
                    f_priceRow(r, ac)
                    r += 1
            needSep := true

        if showP3
            if needSep
                f_sep(r)
                r += 1
            color ac = bullAccent
            f_head(r, "◆ SETUP ANALYTICS", str.tostring(tgtRInput, "#.##") + "R · " + (stopModeInput == "Structural" ? "structural stop" : str.tostring(invUInput, "#.##") + "×ATR stop") + " · INV-first", ac)
            r += 1
            f_strip(r, color.new(ac, 55))
            f_c(1, r, "SETUP", MUTED, COLHD, text.align_left, "")
            table.merge_cells(dash, 1, r, 2, r)
            f_c(3, r, "N", MUTED, COLHD, text.align_right, "")
            f_c(4, r, "WIN%", MUTED, COLHD, text.align_right, "")
            f_c(5, r, "T/O", MUTED, COLHD, text.align_right, "")
            f_c(6, r, "BARS", MUTED, COLHD, text.align_right, "")
            f_c(7, r, "MFE/MAE", MUTED, COLHD, text.align_right, "")
            f_c(8, r, "EDGE", MUTED, COLHD, text.align_left, "")
            r += 1
            f_arow(r, "SWEEP · RAID",   kN.get(0), kW.get(0), kTO.get(0), kBar.get(0), kMfe.get(0), kMae.get(0), colXInput,   ac, "Liquidity sweep: wick through the pool, close back inside on the same bar · reversal thesis (not a structure break)")
            r += 1
            f_arow(r, "SFP · RECLAIM", kN.get(1), kW.get(1), kTO.get(1), kBar.get(1), kMfe.get(1), kMae.get(1), colSfpInput, ac, "Close through the level, then a close back inside · reversal thesis")
            r += 1
            f_arow(r, "MSS · SHIFT", kN.get(2), kW.get(2), kTO.get(2), kBar.get(2), kMfe.get(2), kMae.get(2), colMssInput, ac, "Close through the level and acceptance beyond it · continuation thesis")
            r += 1
            f_arow(r, "ALL", totN, totW, totTO, kBar.get(0) + kBar.get(1) + kBar.get(2), kMfe.get(0) + kMfe.get(1) + kMfe.get(2), kMae.get(0) + kMae.get(1) + kMae.get(2), TXT, ac, "All resolved signals")
            r += 1
            f_kv(r, "Exit Mix", totN > 0 ? "TARGET " + str.tostring(totW) + " · INVALIDATED " + str.tostring(totN - totW - totTO) + " · TIMEOUT " + str.tostring(totTO) : "—", TXT, ac, "Every resolved signal lands in exactly one bucket")
            r += 1
            f_kv(r, "By Side", "LONG ▲ " + f_pct(sW.get(0), sN.get(0)) + " (" + str.tostring(sN.get(0)) + ") · SHORT ▼ " + f_pct(sW.get(1), sN.get(1)) + " (" + str.tostring(sN.get(1)) + ")", f_frac(sW.get(0), sN.get(0)) > f_frac(sW.get(1), sN.get(1)) ? bullAccent : f_frac(sW.get(1), sN.get(1)) > f_frac(sW.get(0), sN.get(0)) ? bearAccent : TXT, ac, "Reversion / continuation rate by thesis direction")
            r += 1
            f_kv(r, "By Strength", "MILD " + f_pct(bW.get(0), bN.get(0)) + " (" + str.tostring(bN.get(0)) + ") · STRONG " + f_pct(bW.get(1), bN.get(1)) + " (" + str.tostring(bN.get(1)) + ") · EXTREME " + f_pct(bW.get(2), bN.get(2)) + " (" + str.tostring(bN.get(2)) + ")", TXT, ac, "Does a higher strength score win more often? Check here before trusting the stars.")
            r += 1
            string pfS = sumLossR > 0 ? str.tostring(sumWinR / sumLossR, "#.00") : sumWinR > 0 ? "∞" : "—"
            f_kv(r, "Net R · PF · Max DD", f_sn(totR, "#.0") + "R · " + pfS + " · −" + str.tostring(maxDD, "#.0") + "R", totR >= 0 ? bullAccent : bearAccent, ac, "+target R on a win · −1R on invalidation · mark-to-market on a timeout")
            r += 1
            f_kv(r, "Expectancy · Streak", (totN > 0 ? f_sn(totR / totN, "#.00") + "R / signal" : "—") + " · " + (streak > 0 ? "W" + str.tostring(streak) : streak < 0 ? "L" + str.tostring(-streak) : "—"), totN == 0 ? MUTED : totR >= 0 ? bullAccent : bearAccent, ac, "")
            r += 1
            f_note(r, "every resolved signal counted · same-bar ties booked as losses · timeouts never hidden", ac)
            r += 1
            needSep := true

        if showP4
            if needSep
                f_sep(r)
                r += 1
            color ac = bearAccent
            f_head(r, "◆ SESSION ANALYTICS", "where liquidity is born · how it dies", ac)
            r += 1
            f_colhd(r, array.from("SESSION", "LVLS", "SWP%", "SFP%", "MSS%", "SIG", "WIN%", ""), ac)
            r += 1
            f_srow(r, 1, ac)
            r += 1
            f_srow(r, 2, ac)
            r += 1
            f_srow(r, 3, ac)
            r += 1
            f_srow(r, 4, ac)
            r += 1
            f_srow(r, 0, ac)
            r += 1
            needSep := true

        if showP5
            if needSep
                f_sep(r)
                r += 1
            color ac = bearSoft
            int nEv = evKind.get(0) + evKind.get(1) + evKind.get(2)
            f_head(r, "◆ SWEEP STATISTICS", str.tostring(nEv) + " classified", ac)
            r += 1
            f_kv(r, "Levels", str.tostring(nLvlCreated) + " born · " + str.tostring(nLvlMerged) + " merged · " + str.tostring(nLvlExpired) + " expired · " + str.tostring(nEv) + " swept", TXT, ac, "Expired = retired unswept by the per-side cap or max age")
            r += 1
            f_kv(r, "Resolution Mix", nEv > 0 ? "SWEEP " + f_pct(evKind.get(0), nEv) + " · SFP " + f_pct(evKind.get(1), nEv) + " · MSS " + f_pct(evKind.get(2), nEv) : "—", TXT, ac, "How swept liquidity resolves on this symbol / timeframe")
            r += 1
            int nRev = evKind.get(0) + evKind.get(1)
            f_kv(r, "Reclaim Rate", nEv > 0 ? f_pct(nRev, nEv) + " reclaimed (SWEEP + SFP) · " + f_pct(evKind.get(2), nEv) + " accepted (MSS)" : "—", nEv == 0 ? MUTED : f_frac(nRev, nEv) >= 0.55 ? bullAccent : f_frac(nRev, nEv) <= 0.45 ? bearAccent : TXT, ac, "High reclaim rate = a market that fakes through liquidity · low = a market that trends through it")
            r += 1
            f_kv(r, "Avg Sweep Depth", nEv > 0 ? "SWEEP " + (evKind.get(0) > 0 ? str.tostring(evDepth.get(0) / evKind.get(0), "#.00") : "—") + " · SFP " + (evKind.get(1) > 0 ? str.tostring(evDepth.get(1) / evKind.get(1), "#.00") : "—") + " · MSS " + (evKind.get(2) > 0 ? str.tostring(evDepth.get(2) / evKind.get(2), "#.00") : "—") + " ATR" : "—", TXT, ac, "Average wick / close excursion beyond the level per setup, in ATR")
            r += 1
            f_kv(r, "Avg Level Age At Sweep", nEv > 0 ? "SWEEP " + (evKind.get(0) > 0 ? str.tostring(evAge.get(0) / evKind.get(0), "#") : "—") + " · SFP " + (evKind.get(1) > 0 ? str.tostring(evAge.get(1) / evKind.get(1), "#") : "—") + " · MSS " + (evKind.get(2) > 0 ? str.tostring(evAge.get(2) / evKind.get(2), "#") : "—") + " bars" : "—", TXT, ac, "Bars from the pivot to the resolution bar")
            r += 1
            string calS = nLvlCreated < 20 ? "collecting…" : f_frac(nLvlExpired, nLvlCreated) > 0.5 ? "MANY EXPIRE UNSWEPT · raise Max Active or Pivot Period" : f_frac(nEv, nLvlCreated) > 0.9 ? "ALMOST EVERYTHING SWEPT · lower Pivot Period for finer pools" : "CALIBRATED"
            f_kv(r, "Calibration", calS, nLvlCreated < 20 ? MUTED : str.contains(calS, "CALIBRATED") ? bullAccent : GOLD, ac, "Target: most pools get swept before they expire")
            r += 1

alertcondition(newLvlBear,                 title="SFP MSS · New Sell-Side Liquidity",  message="Strong SFP MSS {{ticker}} {{interval}}: new sell-side liquidity pool confirmed")
alertcondition(newLvlBull,                 title="SFP MSS · New Buy-Side Liquidity",   message="Strong SFP MSS {{ticker}} {{interval}}: new buy-side liquidity pool confirmed")
alertcondition(evXBull,                    title="SFP MSS · SWEEP · Bullish",          message="Strong SFP MSS {{ticker}} {{interval}}: liquidity sweep of buy-side pool · bullish same-bar reclaim")
alertcondition(evXBear,                    title="SFP MSS · SWEEP · Bearish",          message="Strong SFP MSS {{ticker}} {{interval}}: liquidity sweep of sell-side pool · bearish same-bar reclaim")
alertcondition(evSfpBull,                  title="SFP MSS · SFP · Bullish",            message="Strong SFP MSS {{ticker}} {{interval}}: SFP reclaim above buy-side liquidity · bullish")
alertcondition(evSfpBear,                  title="SFP MSS · SFP · Bearish",            message="Strong SFP MSS {{ticker}} {{interval}}: SFP reclaim below sell-side liquidity · bearish")
alertcondition(evMssBull,                  title="SFP MSS · MSS · Bullish",            message="Strong SFP MSS {{ticker}} {{interval}}: MSS acceptance above sell-side liquidity · bullish shift")
alertcondition(evMssBear,                  title="SFP MSS · MSS · Bearish",            message="Strong SFP MSS {{ticker}} {{interval}}: MSS acceptance below buy-side liquidity · bearish shift")
alertcondition(sigNow,                     title="SFP MSS · Signal (any)",             message="Strong SFP MSS {{ticker}} {{interval}}: tracked liquidity signal fired")
alertcondition(sigStrongNow,               title="SFP MSS · EXTREME Signal (7.5+)",    message="Strong SFP MSS {{ticker}} {{interval}}: EXTREME liquidity signal (7.5+)")
alertcondition(outTgtNow,                  title="SFP MSS · Signal Target Hit",        message="Strong SFP MSS {{ticker}} {{interval}}: a liquidity signal reached its target")
alertcondition(outInvNow,                  title="SFP MSS · Signal Invalidated",       message="Strong SFP MSS {{ticker}} {{interval}}: a liquidity signal was invalidated")
alertcondition(nearUpNow,                  title="SFP MSS · Approaching Sell-Side Liquidity", message="Strong SFP MSS {{ticker}} {{interval}}: price within the proximity window of sell-side liquidity")
alertcondition(nearDnNow,                  title="SFP MSS · Approaching Buy-Side Liquidity",  message="Strong SFP MSS {{ticker}} {{interval}}: price within the proximity window of buy-side liquidity")
alertcondition(pdhSwept and not pdhSwept[1], title="SFP MSS · PDH Swept",             message="Strong SFP MSS {{ticker}} {{interval}}: prior-day high swept")
alertcondition(pdlSwept and not pdlSwept[1], title="SFP MSS · PDL Swept",             message="Strong SFP MSS {{ticker}} {{interval}}: prior-day low swept")
alertcondition(pwhSwept and not pwhSwept[1], title="SFP MSS · PWH Swept",             message="Strong SFP MSS {{ticker}} {{interval}}: prior-week high swept")
alertcondition(pwlSwept and not pwlSwept[1], title="SFP MSS · PWL Swept",             message="Strong SFP MSS {{ticker}} {{interval}}: prior-week low swept")
